/*
 * Copyright 2010 Alexander Ulizko
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ulizko.template.soy;

import com.ulizko.template.soy.javaSrc.*;
import com.ulizko.template.soy.utils.FileUtils;
import com.ulizko.template.soy.utils.StringUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.google.template.soy.javasrc.SoyJavaSrcOptions;
import com.google.template.soy.shared.SoyGeneralOptions;
import com.google.template.soy.xliffmsgplugin.XliffMsgPluginModule;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;

import java.io.File;
import java.util.List;
import java.util.Map;


/**
 * Soy Java Backend Generator.
 * Lots of code (command-line arguments, command-line arguments parsing borrowed from {@link com.google.template.soy.SoyToJavaSrcCompilerExperimental}.
 * Suites for shell-execution.
 * <p>
 * Usage:
 * java -jar .:SoyToJavaSrcComppilerOMatic.jar \
 *     [<flag1> <flag2> ...] --outputDirectory <outputDirectoryPath>  \
 *     <soyFile1> <soyFile2> ...
 *
 * Where soyFile1, soyFile2 paths to soy files (with extension .soy) or directories.
 * On second case, directory will be recursively scanned for files with .soy extension.
 *
 * You can pass same arguments as you would for {@link com.google.template.soy.SoyToJavaSrcCompilerExperimental}.
 * The only difference is that I added --dropTargetDirectory flag,
 * which indicates whether that tool should clear target directory before execution or not.
 * By default it set to false (shocking, huh?).
 *
 * @author Alexander <alexander@ulizko.com> Ulizko
 */
public class SoyToJavaSrcCompilerOMaticShell {
    private static final String USAGE_PREFIX =
            "Usage:\n" +
                    "java -jar SoyToJavaSrcComppilerOMatic.jar \\\n" +
                    "     [<flag1> <flag2> ...] --outputDirectory <outputDirectoryPath>  \\\\\\n" +
                    "     <soyFile1> <soyFile2> ...\\n\n\n" +
                    "Where soyFile1, soyFile2 paths to soy files (with extension .soy) or directories. \n" +
                    "On second case, directory will be recursively scanned for files with .soy extension.\n\n\n";

    @Option(name = "--dropTargetDirectory",
            usage = "If provided, deletes recursively content of outputDirectory.")
    private Boolean dropTargetDirectoryBeforeGeneration = false;

    @Option(name = "--outputDirectory",
            usage = "The path to the output directory. Will be created if not exists.")
    private String outputDirectoryPath = "";

    @Option(name = "--codeStyle",
            usage = "The code style to use when generating Java code ('stringbuilder' or 'concat').")
    private SoyJavaSrcOptions.CodeStyle codeStyle = SoyJavaSrcOptions.CodeStyle.STRINGBUILDER;

    @Option(name = "--messageFilePath",
            usage = "The path to the translated messages file. If not provided, messages from the" +
                    " Soy source are used.")
    private String messageFilePath = "";

    @Option(name = "--bidiGlobalDir",
            usage = "The bidi global directionality (ltr=1, rtl=-1) to use when processing bidi" +
                    " functions/directives. Only applicable if your Soy code uses bidi" +
                    " functions/directives. Also note that this flag is usually not necessary if" +
                    " a message file is provided, because in that case the bidi global" +
                    " directionality is simply inferred from the message bundle.")
    private int bidiGlobalDir = 0;

    @Option(name = "--cssHandlingScheme",
            usage = "The scheme to use for handling 'css' commands. Specifying 'literal' will" +
                    " cause command text to be inserted as literal text. Specifying 'reference'" +
                    " will cause command text to be evaluated as a data or global reference. This" +
                    " option has no effect if the Soy code does not contain 'css' commands.")
    private String cssHandlingScheme = "literal";

    @Option(name = "--compileTimeGlobalsFile",
            usage = "The path to a file containing the mappings for global names to be substituted" +
                    " at compile time. Each line of the file should have the format" +
                    " \"<global_name> = <primitive_data>\" where primitive_data is a valid Soy" +
                    " expression literal for a primitive type (null, boolean, integer, float, or" +
                    " string). Empty lines and lines beginning with \"//\" are ignored. The file" +
                    " should be encoded in UTF-8. If you need to generate a file in this format" +
                    " from Java, consider using the utility" +
                    " SoyUtils.generateCompileTimeGlobalsFile().")
    private String compileTimeGlobalsFile = "";

    @Option(name = "--messagePluginModule",
            usage = "Specifies the full class name of a Guice module that binds a SoyMsgPlugin." +
                    " If not specified, the default is" +
                    " com.google.template.soy.xliffmsgplugin.XliffMsgPluginModule, which binds" +
                    " the XliffMsgPlugin.")
    private String messagePluginModule = XliffMsgPluginModule.class.getName();


    @Option(name = "--pluginModules",
            usage = "Specifies the full class names of Guice modules for function plugins and" +
                    " print directive plugins (comma-delimited list).")
    private String pluginModules = "";

    @Option(name = "--soyInfoJavaPackage",
            usage = "If you want soyInfo files to be generated, enter package name for them.\n" +
                    "If you won't pass --soyInfoOutputDirectory argument also, path to generated files will be \n" +
                    "computed automatically (Recommended).")
    private String soyInfoJavaPackage = "";

    @Option(name = "--soyInfoOutputDirectory",
            usage = "Output directory for generated SoyInfo files. Optional. \n" +
                    "If not provided, --outputDirectoryPath will be used.\n" +
                    "If you want to use this options, you must pass valid package name as the \n" +
                    "--soyInfoJavaPackage argument value.")
    private String soyInfoOutputDirectory = "";


    @Option(name = "--soyInfoClassNameSource",
            usage = "The source for the generated class names. Used for SoyInfo files generation. " +
                    "Valid values are: " +
                  " \"filename\", \"namespace\", and \"generic\". Option \"filename\" turns" +
                  " a Soy file name AaaBbb.soy or aaa_bbb.soy into AaaBbbSoyInfo. Option" +
                  " \"namespace\" turns a namespace aaa.bbb.cccDdd into CccDddSoyInfo (note" +
                  " it only uses the last part of the namespace). Option \"generic\" generates" +
                  " class names such as File1SoyInfo, File2SoyInfo.\n" +
                  "Default value is \"filename\".")
    private String soyInfoJavaClassNameSource = "";

    /**
     * The remaining arguments after parsing command-line flags.
     */
    @Argument
    private List<String> arguments = Lists.newArrayList();

    public static void main(String[] args) throws Exception {
        (new SoyToJavaSrcCompilerOMaticShell()).processMain(args);

    }

    private SoyToJavaSrcCompilerOMaticShell() {
    }

    private void processMain(String[] args) throws Exception {
        CmdLineParser cmdLineParser = parseFlags(this, args, USAGE_PREFIX);
        if (arguments.size() == 0) {
            exitWithError("Must provide list of Soy files.", cmdLineParser, USAGE_PREFIX);
        }

        // update output path so that it ends with File.separator.
        this.outputDirectoryPath = StringUtils.addTrailingFileSeparatorToPathIfNone(this.outputDirectoryPath);
        

        Preconditions.checkArgument(outputDirectoryPath.length() > 0, "You should provide non-empty path to output directory");

        FileUtils.ensureDirsExistInPath(outputDirectoryPath);

        File outputDirectory = new File(outputDirectoryPath);

        if (dropTargetDirectoryBeforeGeneration) {
            Files.deleteDirectoryContents(outputDirectory);
        }

        // Create SoyJavaSrcOptions.
        SoyJavaSrcOptions javaSrcOptions = new SoyJavaSrcOptions();
        javaSrcOptions.setCodeStyle(codeStyle);
        javaSrcOptions.setBidiGlobalDir(bidiGlobalDir);

        SoyFileSetBuilder soyFileSetBuilder = new SoyFileSetBuilder();

        soyFileSetBuilder.setCssHandlingScheme(SoyGeneralOptions.CssHandlingScheme.valueOf(cssHandlingScheme.toUpperCase()));
        soyFileSetBuilder.setMessageFilePath(messageFilePath);
        soyFileSetBuilder.setJavaSrcOptions(javaSrcOptions);
        soyFileSetBuilder.setCompileTimeGlobalsFile(compileTimeGlobalsFile);
        soyFileSetBuilder.setPluginModules(pluginModules);
        soyFileSetBuilder.setMessagePluginModule(messagePluginModule);

        SoyFileStock soyFileStock = new SoyFileStock(arguments);

        soyFileStock.checkFiles();

        soyFileSetBuilder.buildSoyFileSet(ImmutableList.copyOf(soyFileStock.getFileNames()));


        String generatedCode = soyFileSetBuilder.compileToJavaSrc();

        BlobJavaSrcAnalyzer blobJavaSrcAnalyzer = new BlobJavaSrcAnalyzer();
        blobJavaSrcAnalyzer.analyze(generatedCode);

        ImmutableList<String> chunks = ImmutableList.copyOf(Splitter.on(File.separatorChar).split(outputDirectory.getAbsolutePath()));

        for (JavaClassBlank javaClassBlank : blobJavaSrcAnalyzer.getBlankClasses().values()) {
            String path = StringUtils.addTrailingFileSeparatorToPathIfNone(outputDirectory.getAbsolutePath()) +
                    StringUtils.generateDirsPathForFileByPackage(chunks,
                            ImmutableList.copyOf(Splitter.on(".").split(javaClassBlank.getPackageName()))) +
                    javaClassBlank.getClassName() + ".java";


            File file = new File(path);
            FileUtils.ensureDirsExistInPath(path);
            Files.write(javaClassBlank.generateFileContent(), file, Charsets.UTF_8);
        }

        if (soyInfoJavaPackage.length() > 0) {
            soyFileSetBuilder.setJavaPackageName(soyInfoJavaPackage);
            if (soyInfoJavaClassNameSource.length() == 0) {
                soyInfoJavaClassNameSource = "filename";
            }
            soyFileSetBuilder.setJavaClassNameSource(soyInfoJavaClassNameSource);

            Map<String, String> generatedFiles = soyFileSetBuilder.generateParseInfo();

            String path = "";

            if (soyInfoOutputDirectory.length() > 0) {
                path = soyInfoOutputDirectory;
            } else {
                path = StringUtils.addTrailingFileSeparatorToPathIfNone(outputDirectory.getAbsolutePath()) + StringUtils.generateDirsPathForFileByPackage(chunks, ImmutableList.copyOf(Splitter.on(".").split(soyInfoJavaPackage)));
            }


            for (Map.Entry<String, String> entry : generatedFiles.entrySet()) {
                File outputFile = new File(path, entry.getKey());
                FileUtils.ensureDirsExistInPath(outputFile.getPath());
                Files.write(entry.getValue(), outputFile, Charsets.UTF_8);
            }
        }

    }


    /**
     * Prints an error message and the usage string, and then exits.
     * <p/>
     * Borrowed from {@link com.google.template.soy.MainClassUtils}.
     *
     * @param errorMsg      The error message to print.
     * @param cmdLineParser The CmdLineParser used to print usage text for flags.
     * @param usagePrefix   The string to prepend to the usage message (when reporting an error).
     */
    public static void exitWithError(
            String errorMsg, CmdLineParser cmdLineParser, String usagePrefix) {

        System.err.println("\nError: " + errorMsg + "\n\n");
        System.err.println(usagePrefix);
        cmdLineParser.printUsage(System.err);

        System.exit(1);
    }

    /**
     * Parses command line flags written with args4j.
     * <p/>
     * Borrowed from {@link com.google.template.soy.MainClassUtils}
     *
     * @param objWithFlags An instance of a class containing args4j flag definitions.
     * @param args         The args string to parse.
     * @param usagePrefix  The string to prepend to the usage message (when reporting an error).
     * @return The CmdLineParser that was created and used to parse the args (can be used to print
     *         usage text for flags when reporting errors).
     */
    public static CmdLineParser parseFlags(Object objWithFlags, String[] args, String usagePrefix) {

        CmdLineParser cmdLineParser = new CmdLineParser(objWithFlags);
        cmdLineParser.setUsageWidth(100);

        try {
            cmdLineParser.parseArgument(args);

        } catch (CmdLineException cle) {
            exitWithError(cle.getMessage(), cmdLineParser, usagePrefix);
        }

        return cmdLineParser;
    }
}
